package com.element.oauth2.server.global.provider;

import com.common.core.exception.code.SecurityErrorCode;
import com.element.oauth2.constant.SecurityParams;
import com.element.oauth2.exception.ClientAuthenticationException;
import com.element.oauth2.model.entity.AuthUserE;
import com.element.oauth2.server.global.token.ParamAuthenticationToken;
import com.element.oauth2.server.service.IAuthUserDetailsService;
import com.element.oauth2.utils.OAuth2Util;
import io.netty.util.internal.StringUtil;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.core.OAuth2Token;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
import org.springframework.util.CollectionUtils;

import java.security.Principal;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;

public class ParamAuthenticationProvider implements AuthenticationProvider {

    private final PasswordEncoder passwordEncoder;

    private final IAuthUserDetailsService authUserDetailsService;

    private final OAuth2AuthorizationService authorizationService;

    private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;


    public ParamAuthenticationProvider(PasswordEncoder passwordEncoder,
                                       IAuthUserDetailsService authUserDetailsService,
                                       OAuth2AuthorizationService authorizationService,
                                       OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) {
        this.tokenGenerator = tokenGenerator;
        this.passwordEncoder = passwordEncoder;
        this.authorizationService = authorizationService;
        this.authUserDetailsService = authUserDetailsService;
    }

    @Override
    public Authentication authenticate(Authentication authentication) {
        ParamAuthenticationToken authenticationToken = (ParamAuthenticationToken) authentication;
        Map<String, String> params = authenticationToken.getParams();
        if (CollectionUtils.isEmpty(params)) {
            throw new InternalAuthenticationServiceException("params is null");
        }
        AuthUserE authUser;
        try {
            if (AuthorizationGrantType.PASSWORD.getValue().equals(authenticationToken.getAuthorizationGrantType().getValue())) {
                String userName = params.get(SecurityParams.USER_NAME);
                String password = params.get(SecurityParams.PASSWORD);
                if (StringUtil.isNullOrEmpty(userName) || StringUtil.isNullOrEmpty(password)) {
                    throw ClientAuthenticationException.ERROR(SecurityErrorCode.A_INSUFFICIENT_AUTHENTICATION_ERROR);
                }
                authUser = authUserDetailsService.loadUserByUsername(userName);
                if (null != authUser) {
                    if (!passwordEncoder.matches(password, authUser.getPassword())) {
                        throw ClientAuthenticationException.ERROR(SecurityErrorCode.A_USERNAME_OR_PASSWORD_ERROR);
                    }
                }
            } else {
                authUser = authUserDetailsService.loadUserByParams(params);
            }
            authenticationToken.setPrincipal(authUser);
        } catch (ClientAuthenticationException e) {
            throw e;
        } catch (UsernameNotFoundException e) {
            throw ClientAuthenticationException.ERROR(SecurityErrorCode.A_USERNAME_NOT_FOUND_EXCEPTION);
        } catch (Exception e) {
            throw ClientAuthenticationException.ERROR(SecurityErrorCode.A_INVALID_GRANT);
        }
        if (null == authUser) {
            throw ClientAuthenticationException.ERROR(SecurityErrorCode.A_USERNAME_NOT_FOUND_EXCEPTION);
        }

        OAuth2ClientAuthenticationToken clientPrincipal = (OAuth2ClientAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
        RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
        if (null == registeredClient) {
            throw ClientAuthenticationException.ERROR(SecurityErrorCode.A_INVALID_CLIENT_EXCEPTION);
        }
        Set<String> urls = new HashSet<>();
        if (!CollectionUtils.isEmpty(registeredClient.getScopes())) {
            String scopes = params.get(SecurityParams.SCOPE);
            if (StringUtil.isNullOrEmpty(scopes)) {
                throw ClientAuthenticationException.ERROR(SecurityErrorCode.A_INVALID_SCOPE);
            }
            Set<String> requestScopes = OAuth2Util.parseParameterList(scopes);
            // 验证scope是否正确
            validateScope(requestScopes, registeredClient.getScopes());

            ClientSettings clientSettings = registeredClient.getClientSettings();
            if (null != clientSettings) {
                Map<String, Set<String>> scopesMap = clientSettings.getSetting(SecurityParams.SCOPE);
                if (!CollectionUtils.isEmpty(scopesMap)) {
                    requestScopes.forEach(scope -> urls.addAll(scopesMap.get(scope)));
                }
            }
        }
        Set<AuthorizationGrantType> grantTypes = registeredClient.getAuthorizationGrantTypes();
        if (!CollectionUtils.isEmpty(grantTypes)) {
            AtomicBoolean hasType = new AtomicBoolean(false);
            grantTypes.forEach(authorizationGrantType -> {
                if (authenticationToken.getAuthorizationGrantType().getValue().equals(authorizationGrantType.getValue())) {
                    hasType.set(true);
                }
            });
            if (!hasType.get()) {
                throw ClientAuthenticationException.ERROR(SecurityErrorCode.A_UNSUPPORTED_GRANT_TYPE);
            }
        }

        OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization
                .withRegisteredClient(registeredClient)
                .id(authUser.getUserId())
                .principalName(registeredClient.getClientName())
                .authorizationGrantType(authenticationToken.getAuthorizationGrantType())
                // 0.4.0 新增的方法
                .authorizedScopes(registeredClient.getScopes());
        authorizationBuilder.attribute(SecurityParams.CLIENT_ID, registeredClient.getClientId());
        authorizationBuilder.attribute(SecurityParams.USER_INFO, authUser);
        authorizationBuilder.attribute(SecurityParams.SCOPE, urls);
        authorizationBuilder.attribute(Principal.class.getName(), authenticationToken);

        OAuth2AccessToken oAuth2AccessToken;
        OAuth2RefreshToken oAuth2RefreshToken = null;
        // 生成accessToken
        DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
                .registeredClient(registeredClient)
                .principal(authenticationToken)
                .authorizationServerContext(AuthorizationServerContextHolder.getContext())
                .authorizedScopes(registeredClient.getScopes())
                .authorizationGrantType(authenticationToken.getAuthorizationGrantType());

        OAuth2TokenContext accessTokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build();
        OAuth2Token oAuthAccessToken = this.tokenGenerator.generate(accessTokenContext);
        if (oAuthAccessToken == null) {
            throw ClientAuthenticationException.ERROR(SecurityErrorCode.A_INVALID_GRANT);
        }
        oAuth2AccessToken = (OAuth2AccessToken) oAuthAccessToken;
        authorizationBuilder.accessToken(oAuth2AccessToken);

        if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN)) {
            OAuth2TokenContext RefreshTokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build();
            OAuth2Token oAuthRefreshToken = this.tokenGenerator.generate(RefreshTokenContext);
            if (null != oAuthRefreshToken) {
                oAuth2RefreshToken = (OAuth2RefreshToken) oAuthRefreshToken;
                authorizationBuilder.refreshToken(oAuth2RefreshToken);
            }
        }
        OAuth2Authorization authorization = authorizationBuilder.build();

        authorizationService.save(authorization);
        if (null == oAuth2RefreshToken) {
            return new OAuth2AccessTokenAuthenticationToken(registeredClient, authenticationToken, oAuth2AccessToken);
        } else {
            return new OAuth2AccessTokenAuthenticationToken(registeredClient, authenticationToken, oAuth2AccessToken, oAuth2RefreshToken);
        }
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return ParamAuthenticationToken.class.isAssignableFrom(authentication);
    }

    private void validateScope(Set<String> requestScopes, Set<String> clientScopes) {
        if (clientScopes != null && !clientScopes.isEmpty()) {
            for (String scope : requestScopes) {
                if (!clientScopes.contains(scope)) {
                    throw ClientAuthenticationException.ERROR(SecurityErrorCode.A_INVALID_SCOPE);
                }
            }
        } else {
            throw ClientAuthenticationException.ERROR(SecurityErrorCode.A_INVALID_SCOPE, "无法访问该领域请求");
        }
        if (requestScopes.isEmpty()) {
            throw ClientAuthenticationException.ERROR(SecurityErrorCode.A_INVALID_SCOPE, "请求领域为空");
        }
    }
}
